﻿using System;
using System.Threading.Tasks;

namespace Multiprocessing.Core.Mathematics.Matrices
{
    public abstract class BaseMatrix : IMatrix
    {
        #region Fields

        private readonly RowCollection _rows;
        private readonly ColumnCollection _columns;

        #endregion

        #region Constructors

        protected BaseMatrix(int rowCount, int columnCount)
        {
            if (rowCount < 0)
            {
                throw new ArgumentException("rowCount < 0");
            }
            if (columnCount < 0)
            {
                throw new ArgumentException("columnCount < 0");
            }
            _rows = new RowCollection(this);
            _columns = new ColumnCollection(this);
            RowCount = rowCount;
            ColumnCount = columnCount;
            Initialize();
        }



        #endregion

        #region Properties

        public int ColumnCount { get; protected set; }

        public int RowCount { get; protected set; }

        public virtual double this[int row, int column]
        {
            get { return GetElement(row, column); }
            set { SetElement(row, column, value); }
        }

        public RowCollection Rows { get { return _rows; } }

        public ColumnCollection Columns { get { return _columns; } }

        public static MatrixFactory Factory
        {
            get { return MatrixFactory.Instance; }
        }

        #endregion

        public object Clone()
        {
            return CloneMatrix();
        }

        public void Inverse()
        {
            Parallel.For(1, this.RowCount, row =>
                                               {
                                                   for (int column = 0; column < row; ++column)
                                                   {
                                                       var buffer = this[row, column];
                                                       this[row, column] = this[column, row];
                                                       this[column, row] = buffer;
                                                   }
                                               });
        }

        private void Initialize()
        {
            CreateUnderlaying(ColumnCount, RowCount);
        }

        protected abstract void CreateUnderlaying(int row, int column);

        protected abstract IMatrix CloneMatrix();

        public abstract double GetElement(int row, int column);

        public abstract void SetElement(int row, int column, double value);

        public virtual IMatrixPart GetRow(int index)
        {
            return new MatrixRow(this, index);
        }

        public virtual IMatrixPart GetColumn(int index)
        {
            return new MatrixColumn(this, index);
        }

        public abstract IMatrix Add(IMatrix matrix);

        public abstract IMatrix Substract(IMatrix matrix);

        public abstract IMatrix Multiply(IMatrix matrix);

        public abstract IMatrix Multiply(double value);

        public abstract IMatrix Divide(double value);
        
        protected static void Divide(IMatrix first, double value, IMatrix result)
        {
            if (ReferenceEquals(first, null))
            {
                throw new ArgumentNullException("first");
            }
            if (ReferenceEquals(result, null))
            {
                throw new ArgumentNullException("result");
            }
            if (first.RowCount != result.RowCount || first.ColumnCount != result.ColumnCount)
            {
                throw new ArgumentException();
            }
            Parallel.For(0, first.RowCount, row =>
                                                {
                                                    for (int column = 0; column < first.ColumnCount; ++column)
                                                    {
                                                        result[row, column] = first[row, column]/value;
                                                    }
                                                });
        }

        protected static void CloneMatrix(IMatrix source, IMatrix destination)
        {
            if (ReferenceEquals(source, null))
            {
                throw new ArgumentNullException("source");
            }
            if (ReferenceEquals(destination, null))
            {
                throw new ArgumentNullException("destination");
            }
            if (source.ColumnCount != destination.ColumnCount || source.RowCount != destination.RowCount)
            {
                throw new ArgumentException("Matrix should be the same size");
            }
            Parallel.For(0, source.RowCount, row =>
                                                 {
                                                     for (int column = 0; column < source.ColumnCount; ++column)
                                                     {
                                                         destination[row, column] = source[row, column];
                                                     }
                                                 });
        }

        protected static void Add(IMatrix first, IMatrix second, IMatrix result)
        {
            if (ReferenceEquals(first, null))
            {
                throw new ArgumentNullException("first");
            }
            if (ReferenceEquals(second, null))
            {
                throw new ArgumentNullException("second");
            }
            if (ReferenceEquals(result, null))
            {
                throw new ArgumentNullException("result");
            }
            if (first.RowCount != second.RowCount || first.ColumnCount != second.ColumnCount)
            {
                throw new ArgumentException();
            }
            Parallel.For(0, first.ColumnCount, column =>
                                                  {
                                                      for (int row = 0; row < first.RowCount; ++row)
                                                      {
                                                          result[row, column] =
                                                              first[row, column] + second[row, column];
                                                      }
                                                  }
                );
        }

        protected static void Substract(IMatrix first, IMatrix second, IMatrix result)
        {
            if (ReferenceEquals(first, null))
            {
                throw new ArgumentNullException("first");
            }
            if (ReferenceEquals(second, null))
            {
                throw new ArgumentNullException("second");
            }
            if (ReferenceEquals(result, null))
            {
                throw new ArgumentNullException("result");
            }
            if (first.RowCount != second.RowCount || first.ColumnCount != second.ColumnCount)
            {
                throw new ArgumentException();
            }
            Parallel.For(0, first.ColumnCount, column =>
                                                   {
                                                       for (int row = 0; row < first.RowCount; ++row)
                                                       {
                                                           result[row, column] =
                                                               first[row, column] - second[row, column];
                                                       }
                                                   }
                );
        }

        protected static void Multiply(IMatrix first, IMatrix second, IMatrix result)
        {
            if (ReferenceEquals(first, null))
            {
                throw new ArgumentNullException("first");
            }
            if (ReferenceEquals(second, null))
            {
                throw new ArgumentNullException("second");
            }
            if (ReferenceEquals(result, null))
            {
                throw new ArgumentNullException("result");
            }
            if (first.ColumnCount != second.RowCount)
            {
                throw new ArgumentException("first.ColumnCount should be equal second.RowCount");
            }
            if (result.RowCount != first.RowCount || result.ColumnCount != second.ColumnCount)
            {
                throw new ArgumentException("incorrect result matrix");
            }
            // on row of the first matrix
            for (int i = 0; i < first.RowCount; ++i)
            {
                // select row of the second matrix
                for (int j = 0; j < second.ColumnCount; ++j)
                {
                    double value = 0.0;
                    for (int r = 0; r < second.RowCount; ++r)
                    {
                        value += first[i, r]*second[r, j];
                    }
                    result[i, j] = value;
                }
            }
        }

        protected static void Multiply(IMatrix first, double second, IMatrix result)
        {
            if (ReferenceEquals(first, null))
            {
                throw new ArgumentNullException("first");
            }
            if (ReferenceEquals(result, null))
            {
                throw new ArgumentNullException("result");
            }
            if (first.ColumnCount != result.ColumnCount || first.RowCount != result.RowCount)
            {
                throw new ArgumentException();
            }
            Parallel.For(0, first.RowCount, row =>
                                                {
                                                    for (int column = 0; column < first.ColumnCount; column++)
                                                    {
                                                        result[row, column] = first[row, column]*second;
                                                    }
                                                });
        }


    }
}
